package com.xl.rentkeeper.net;

import android.os.Handler;

import com.squareup.wire.Wire;
import com.xl.fuse.protos.packet.Packet;
import com.xl.rentkeeper.App;
import com.xl.rentkeeper.log.QLog;
import com.xl.rentkeeper.util.SLog;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * Created by zjm on 2014/11/8.
 */
@SuppressWarnings("ResourceType")
public class DUTransfer {
    private Wire wire = App.getWire();
    IDUCallback callback;
    private String hostAddress;
    private int port;
    private Selector selector;
    private SocketChannel socket;

    // The buffer into which we'll onRead data when it's available
    private ByteBuffer readBuffer = null;
    private int duLen = -1;
    private ByteBuffer lengthBuffer = ByteBuffer.allocate(4);

    boolean monitorWrite = false;
    private ByteBuffer writeBuffer;

    static final int CurVersion = 1;

    private Handler callbackHandler;
    SLog log = new SLog(NetworkManager.tag+"."+this.getClass().getSimpleName(),true,true);


    public DUTransfer(String host, int port,IDUCallback callback,Handler callbackHandler){
        this.callback = callback;
        this.hostAddress = host;
        this.port = port;
        this.callbackHandler = callbackHandler;
        new Thread(new Runnable() {
            @Override
            public void run() {
                runLoop();
            }
        },"nettcp").start();
    }


    public boolean send(final Packet cmd){
        if (writeBuffer != null)
            return false;
        byte[] body = cmd.toByteArray();
        int duLen = 4 +4+ body.length;
        writeBuffer = ByteBuffer.allocate(duLen);
        writeBuffer.order(ByteOrder.BIG_ENDIAN);
        writeBuffer.putInt(duLen);
        writeBuffer.putInt(CurVersion);
        writeBuffer.put(body);
        writeBuffer.position(0);
        monitorWrite = true;
        if (this.selector != null)
            this.selector.wakeup();
        return true;
    }

    int getMonitorEvent() {
        int monitor  = SelectionKey.OP_READ;
        if (monitorWrite)
            monitor = monitor | SelectionKey.OP_WRITE;
        return  monitor;
    }

    void init() throws IOException {
        this.selector = Selector.open();
        socket = SocketChannel.open();
        socket.socket().connect(new InetSocketAddress(this.hostAddress, this.port),15*1000);
    }

    void runLoop() {
        try {
            log.log("start connect "+hostAddress+":"+port);
            init();
            socket.configureBlocking(false);
            socket.register(selector, getMonitorEvent());
        } catch (final Exception e) {
            e.printStackTrace();
            callbackHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (callback != null)
                        callback.onError(e);
                }
            });
            return ;
        }
        callbackHandler.post(new Runnable() {
            @Override
            public void run() {
                if (callback != null)
                    callback.onConnConnect();
            }
        });
        while (true) {
            try {
                SelectionKey key = socket.keyFor(this.selector);
                key.interestOps(getMonitorEvent());
                int num = this.selector.select();
                Iterator selectedKeys = this.selector.selectedKeys().iterator();
                while (selectedKeys.hasNext()) {
                    SelectionKey skey = (SelectionKey) selectedKeys.next();
                    selectedKeys.remove();
                    if (!skey.isValid()) {
                        continue;
                    }
                    if (skey.isReadable()) {
                        this.onRead(key);
                    } else if (skey.isWritable()) {
                        this.onWrite(key);
                    }
                }
            } catch (Exception e) {
                onError(e);
                return ;
            }
        }
    }

    private void onRead(SelectionKey key) throws Exception {
        log.log("onRead");
        SocketChannel socketChannel = (SocketChannel) key.channel();
        if (duLen == -1) {
            int numRead;
            numRead = socketChannel.read(lengthBuffer);
            if (numRead == -1) {
                throw new IOException("read -1");
            }
            if (numRead == 4) {
                lengthBuffer.position(0);
                duLen = lengthBuffer.order(ByteOrder.BIG_ENDIAN).getInt();
                readBuffer = ByteBuffer.allocate(duLen - 4);
            }
        }
        if (duLen != -1) {
            int numRead;
            numRead = socketChannel.read(readBuffer);
            if (numRead == -1) {
                throw new IOException("read -1");
            }
            // du完整了
            if (!readBuffer.hasRemaining()) {
                ByteBuffer buffer = readBuffer;
                readBuffer = null;
                onDuReady(buffer.array());
            }
        }
    }

    private void onWrite(SelectionKey key) throws IOException {
        log.log("onWrite");
        SocketChannel socketChannel = (SocketChannel) key.channel();
        socketChannel.write(writeBuffer);
        if (!writeBuffer.hasRemaining()) {
            onWriteFin();
        }
    }

    void onError(final Throwable e) {
        log.log("onError:" + e.toString());
//        e.printStackTrace();

        callbackHandler.post(new Runnable() {
            @Override
            public void run() {
                if (callback != null) {
                    callback.onError(e);
                }
            }
        });

        if (socket != null) {
            try {
                socket.close();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            socket = null;
        }
        if (selector != null) {
            try {
                selector.close();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            selector = null;
        }
    }

    void onDuReady(byte[] in) throws Exception {
        log.log("onDuReady:" + duLen);
        lengthBuffer.position(0);
        duLen = -1;
        readBuffer = null;
        if (callback != null) {
            ByteBuffer verB = ByteBuffer.wrap(in, 0, 4);
            int ver = verB.order(ByteOrder.BIG_ENDIAN).getInt();
            if (ver != CurVersion) {
                throw new RuntimeException("wrong version");
            }
            try {
                final Packet packet = wire.parseFrom(in, 4, in.length - 4, Packet.class);
                callbackHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        if (callback != null)
                            callback.onRecvDu(packet);
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
                throw  e;
            }
        }
    }

    void onWriteFin() {
        log.log("onWriteFin len " + writeBuffer.position());
        monitorWrite = false;
        writeBuffer = null;
        callbackHandler.post(new Runnable() {
            @Override
            public void run() {
                if (callback != null)
                    callback.onWriteDuFin();
            }
        });
    }
}
